// Copyright 2016 Proyectos y Sistemas de Mantenimiento SL (eProsima).
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// ------------------------------------------------------------------
// Modification history:
// feature: support for request discovery
// feature: provide query interfaces
// feature: Unified Transport Abstraction Layer Unified Transport Abstraction Layer
// feature: discovery support client and server
// feature: add exception code
// feature: change idl check from md5 to crc16
// ------------------------------------------------------------------

#include <discovery/endpoint/EDPStatic.h>

#include <discovery/participant/PDPSimple.h>
#include <ertps/xmlparser/XMLEndpointParser.h>

#include <edds/rtps/builtin/data/WriterProxyData.h>
#include <edds/rtps/builtin/data/ReaderProxyData.h>
#include <edds/rtps/builtin/data/ParticipantProxyData.h>

#include <edds/rtps/reader/RTPSReader.h>
#include <edds/rtps/writer/RTPSWriter.h>

#include <rtps/participant/RTPSParticipantImpl.h>
#include <tinyxml2.h>

#include <mutex>
#include <sstream>

#include <elog/Log.hpp>

namespace evbs {
namespace ertps {
namespace rtps {

const char* exchange_format_property_name = "dds.discovery.static_edp.exchange_format";
const char* exchange_format_property_value_v1 = "v1";

/**
 * Class Property, used to read and write the strings from the properties used to transmit the
 * EntityId_t.
 */
class EDPStaticProperty {
 public:
    EDPStaticProperty() : m_userId(0U) {}

    ~EDPStaticProperty() {}

    //! Endpoint type
    std::string m_endpointType;
    //! Status
    std::string m_status;
    //! User ID as string
    std::string m_userIdStr;
    //! User ID
    uint16_t m_userId;
    //! Entity ID
    EntityId_t m_entityId;
    //! crc16 idl as string
    std::string m_crc16Idl;

    /**
     * Convert information to a property
     * @param type Type of endpoint
     * @param status Status of the endpoint
     * @param id User Id
     * @param ent EntityId
     * @return Pair of two strings.
     */
    static std::pair<std::string, std::string> toProperty(const EDPStatic::ExchangeFormat exchange_format,
                                                          std::string type, std::string status, uint16_t id,
                                                          const EntityId_t& ent, std::string m_crc16Idl);
    /**
     * @param in_property Input property-
     * @return True if correctly read
     */
    bool fromProperty(std::pair<std::string, std::string> in_property);
};

EDPStatic::EDPStatic(PDP* p, RTPSParticipantImpl* part) : EDP(p, part), mp_edpXML(nullptr) {}

EDPStatic::~EDPStatic() {
    if (mp_edpXML != nullptr) {
        delete (mp_edpXML);
    }
}

bool EDPStatic::initEDP(BuiltinAttributes& attributes) {
    logDebug(RTPS_EDP, "Beginning STATIC EndpointDiscoveryProtocol");

    bool returned_value = false;
    m_attributes = attributes;
    mp_edpXML = new xmlparser::XMLEndpointParser();
    std::string content(m_attributes.discovery_config.static_edp_xml_config());
    if (0U == content.rfind("data://", 0U)) {
        tinyxml2::XMLDocument xml_document;
        if (tinyxml2::XMLError::XML_SUCCESS == xml_document.Parse(content.c_str() + 7, content.size() - 7U)) {
            returned_value = (this->mp_edpXML->loadXMLNode(xml_document) == xmlparser::XMLP_ret::XML_OK);
        }
    } else if (0U == content.rfind("file://", 0U)) {
        std::string file_name = content.substr(7U);
        std::string partiticpantName = this->mp_RTPSParticipant->getAttributes().getName();
        returned_value = (this->mp_edpXML->loadXMLFile(file_name, partiticpantName) == xmlparser::XMLP_ret::XML_OK);
    }

    // Check there is a Participant's property changing the exchange format.
    for (auto& property : mp_RTPSParticipant->getAttributes().properties.properties()) {
        if (0 == property.name().compare(exchange_format_property_name)) {
            if (0 == property.value().compare(exchange_format_property_value_v1)) {
                exchange_format_ = ExchangeFormat::v1;
            } else {
                returned_value = false;
            }
            break;
        }
    }

    return returned_value;
}

std::pair<std::string, std::string> EDPStaticProperty::toProperty(const EDPStatic::ExchangeFormat /*exchange_format*/,
                                                                  std::string type, std::string status, uint16_t id,
                                                                  const EntityId_t& ent, std::string m_crc16Idl) {
    std::pair<std::string, std::string> prop;
    std::stringstream ss;
    ss << type << status << id;
    prop.first = ss.str();
    ss.clear();
    ss.str(std::string());
    ss << (int)ent.value[0] << ".";
    ss << (int)ent.value[1] << ".";
    ss << (int)ent.value[2] << ".";
    ss << (int)ent.value[3] << ".";
    ss << m_crc16Idl;
    prop.second = ss.str();
    return prop;
}

bool EDPStaticProperty::fromProperty(std::pair<std::string, std::string> prop) {
    if (prop.first.substr(0U, 1U) == "R" || prop.first.substr(0U, 1U) == "W") {
        this->m_endpointType = prop.first.substr(0U, 1U);
        this->m_status = prop.first.substr(1U, 1U);
        this->m_userIdStr = prop.first.substr(2U, 100U);
        std::stringstream ss;
        ss << m_userIdStr;
        ss >> m_userId;
        ss.clear();
        ss.str(std::string());
        ss << prop.second;
        int a, b, c, d;
        char ch;
        try {
            ss >> a >> ch >> b >> ch >> c >> ch >> d >> ch;
            ss >> m_crc16Idl;
        } catch (const std::invalid_argument& e) {  //LCOV_EXCL_START
            // stoi threw an std::invalid_argument exception.
            elogError(RTPS_EDP, RetCode_t::RETCODE_BAD_PARAMETER, "Invalid argument: " << e.what());
            return false;
        } catch (const std::out_of_range& e) {
            // stoi threw an std::out_of_range exception.
            elogError(RTPS_EDP, RetCode_t::RETCODE_OUT_OF_RESOURCES, "Out of range: " << e.what());
            return false;
        } catch (const std::exception& e) {
            // stoi threw an std::out_of_range exception.
            elogError(RTPS_EDP, RetCode_t::RETCODE_ERROR, "Other exception: " << e.what());
            return false;
        }  //LCOV_EXCL_STOP
        m_entityId.value[0] = (octet)a;
        m_entityId.value[1] = (octet)b;
        m_entityId.value[2] = (octet)c;
        m_entityId.value[3] = (octet)d;
        return true;
    } else if (0 == prop.first.compare(0U, 4U, "EDS_")) {
        if (0 == prop.first.compare(4U, 1U, "R")) {  //LCOV_EXCL_START
            this->m_endpointType = "Reader";
        } else if (0 == prop.first.compare(4U, 1U, "W")) {
            this->m_endpointType = "Writer";
        } else {
            return false;
        }
        if (0 == prop.first.compare(5U, 1U, "A")) {
            this->m_status = "ALIVE";
        } else if (0 == prop.first.compare(5U, 1U, "E")) {
            this->m_status = "ENDED";
        } else {
            return false;
        }
        this->m_userIdStr = prop.first.substr(7U, 100U);
        std::stringstream ss;
        ss << m_userIdStr;
        ss >> m_userId;
        ss.clear();
        ss.str(std::string());
        ss << prop.second;
        size_t count = std::count(prop.second.begin(), prop.second.end(), '.');
        try {
            int value = 0;
            char ch = 0;
            for (size_t it = 0; it < count; ++it) {
                ss >> value;
                m_entityId.value[3 - (count - it)] = (octet)value;
                if (it != count) {
                    ss >> ch;
                }
            }
            ss >> m_crc16Idl;
        } catch (const std::invalid_argument& e) {
            // stoi threw an std::invalid_argument exception.
            elogError(RTPS_EDP, RetCode_t::RETCODE_BAD_PARAMETER, "Invalid argument: " << e.what());
            return false;
        } catch (const std::out_of_range& e) {
            // stoi threw an std::out_of_range exception.
            elogError(RTPS_EDP, RetCode_t::RETCODE_OUT_OF_RESOURCES, "Out of range: " << e.what());
            return false;
        } catch (const std::exception& e) {
            // stoi threw an std::out_of_range exception.
            elogError(RTPS_EDP, RetCode_t::RETCODE_ERROR, "Other exception: " << e.what());
            return false;
        }
        return true;
    }
    return false;  //LCOV_EXCL_STOP
}

bool EDPStatic::processLocalReaderProxyData(RTPSReader*, ReaderProxyData* rdata) {
    logDebug(RTPS_EDP, rdata->guid().entityId << " in topic: " << rdata->topicName());
    mp_PDP->getMutex()->lock();
    // Add the property list entry to our local pdp
    ParticipantProxyData* localpdata = this->mp_PDP->getLocalParticipantProxyData();
    (void)localpdata->m_properties.push_back(EDPStaticProperty::toProperty(
        exchange_format_, "R", "A", rdata->userDefinedId(), rdata->guid().entityId, rdata->getIdlCrc16()));
    mp_PDP->getMutex()->unlock();
    this->mp_PDP->announceParticipantState(true);
    return true;
}

bool EDPStatic::processLocalWriterProxyData(RTPSWriter*, WriterProxyData* wdata) {
    logDebug(RTPS_EDP, wdata->guid().entityId << " in topic: " << wdata->topicName());
    mp_PDP->getMutex()->lock();
    // Add the property list entry to our local pdp
    ParticipantProxyData* localpdata = this->mp_PDP->getLocalParticipantProxyData();
    (void)localpdata->m_properties.push_back(EDPStaticProperty::toProperty(
        exchange_format_, "W", "A", wdata->userDefinedId(), wdata->guid().entityId, wdata->getIdlCrc16()));
    mp_PDP->getMutex()->unlock();
    this->mp_PDP->announceParticipantState(true);
    return true;
}

bool EDPStatic::removeLocalReader(RTPSReader* R) {
    std::lock_guard<std::recursive_mutex> guard(*mp_PDP->getMutex());
    ParticipantProxyData* localpdata = this->mp_PDP->getLocalParticipantProxyData();
    for (ParameterPropertyList_t::iterator pit = localpdata->m_properties.begin();
         pit != localpdata->m_properties.end(); ++pit) {
        EDPStaticProperty staticproperty;
        if (staticproperty.fromProperty((*pit).pair())) {
            if (staticproperty.m_entityId == R->getGuid().entityId) {
                auto new_property = EDPStaticProperty::toProperty(
                    exchange_format_, "R", "E", static_cast<uint16_t>(R->getAttributes().getUserDefinedID()),
                    R->getGuid().entityId, R->getIdlCrc16());
                if (!pit->modify(new_property)) {
                    elogError(RTPS_EDP, RetCode_t::RETCODE_ERROR,
                              "Failed to change property <" << pit->first() << " | " << pit->second() << "> to <"
                                                            << new_property.first << " | " << new_property.second
                                                            << ">");
                }
            }
        }
    }
    return false;
}

bool EDPStatic::removeLocalWriter(RTPSWriter* W) {
    std::lock_guard<std::recursive_mutex> guard(*mp_PDP->getMutex());
    ParticipantProxyData* localpdata = this->mp_PDP->getLocalParticipantProxyData();
    for (ParameterPropertyList_t::iterator pit = localpdata->m_properties.begin();
         pit != localpdata->m_properties.end(); ++pit) {
        EDPStaticProperty staticproperty;
        if (staticproperty.fromProperty((*pit).pair())) {
            if (staticproperty.m_entityId == W->getGuid().entityId) {
                auto new_property = EDPStaticProperty::toProperty(
                    exchange_format_, "W", "E", static_cast<uint16_t>(W->getAttributes().getUserDefinedID()),
                    W->getGuid().entityId, W->getIdlCrc16());
                if (!pit->modify(new_property)) {
                    elogError(RTPS_EDP, RetCode_t::RETCODE_ERROR,
                              "Failed to change property <" << pit->first() << " | " << pit->second() << "> to <"
                                                            << new_property.first << " | " << new_property.second
                                                            << ">");
                }
            }
        }
    }
    return false;
}

void EDPStatic::assignRemoteEndpoints(const ParticipantProxyData& pdata, bool /*assign_secure_endpoints*/,
                                      bool /*is_request_enable*/) {
    GUID_t persistence_guid;
    for (ParameterPropertyList_t::const_iterator pit = pdata.m_properties.begin(); pit != pdata.m_properties.end();
         ++pit) {
        // cout << "STATIC EDP READING PROPERTY " << pit->first << "// " << pit->second << endl;
        EDPStaticProperty staticproperty;
        if (staticproperty.fromProperty((*pit).pair())) {
            if (staticproperty.m_endpointType == "R" && staticproperty.m_status == "A") {
                GUID_t guid(pdata.m_guid.guidPrefix, staticproperty.m_entityId);
                if (!this->mp_PDP->has_reader_proxy_data(guid))  // IF NOT FOUND, we CREATE AND PAIR IT
                {
                    (void)newRemoteReader(pdata.m_guid, pdata.m_participantName, staticproperty.m_userId,
                                          staticproperty.m_entityId, staticproperty.m_crc16Idl);
                }
            } else if (staticproperty.m_endpointType == "W" && staticproperty.m_status == "A") {
                GUID_t guid(pdata.m_guid.guidPrefix, staticproperty.m_entityId);
                if (!this->mp_PDP->has_writer_proxy_data(guid))  // IF NOT FOUND, we CREATE AND PAIR IT
                {
                    (void)newRemoteWriter(pdata.m_guid, pdata.m_participantName, staticproperty.m_userId,
                                          staticproperty.m_entityId, persistence_guid, staticproperty.m_crc16Idl);
                }
            } else if (staticproperty.m_endpointType == "R" && staticproperty.m_status == "E") {
                GUID_t guid(pdata.m_guid.guidPrefix, staticproperty.m_entityId);
                (void)this->mp_PDP->removeReaderProxyData(guid);
            } else if (staticproperty.m_endpointType == "W" && staticproperty.m_status == "E") {
                GUID_t guid(pdata.m_guid.guidPrefix, staticproperty.m_entityId);
                (void)this->mp_PDP->removeWriterProxyData(guid);
            } else {
                logWarning(RTPS_EDP, "EDPStaticProperty with type: " << staticproperty.m_endpointType << " and status "
                                                                     << staticproperty.m_status << " not recognized");
            }
        } else {
        }
    }
}

bool EDPStatic::newRemoteReader(const GUID_t& participant_guid, const string_255& participant_name, uint16_t user_id,
                                EntityId_t ent_id, std::string m_crc16Idl) {
    ReaderProxyData* rpd = NULL;
    if (mp_edpXML->lookforReader(participant_name.c_str(), user_id, &rpd) == xmlparser::XMLP_ret::XML_OK) {
        logDebug(RTPS_EDP, "Activating: " << rpd->guid().entityId << " in topic " << rpd->topicName());
        GUID_t reader_guid(participant_guid.guidPrefix, ent_id != c_EntityId_Unknown ? ent_id : rpd->guid().entityId);

        auto init_fun = [this, participant_guid, reader_guid, rpd, crc16Idl = std::move(m_crc16Idl)](
                            ReaderProxyData* newRPD, bool updating, const ParticipantProxyData& participant_data,
                            RTPSParticipantImpl* localparticipant) {
            // Should be a new reader
            (void)updating;
            assert(!updating);

            *newRPD = *rpd;
            newRPD->guid(reader_guid);
            if (!checkEntityId(newRPD)) {
                elogError(RTPS_EDP, RetCode_t::RETCODE_PRECONDITION_NOT_MET,
                          "The provided entityId for Reader with ID: " << newRPD->userDefinedId()
                                                                       << " does not match the topic Kind");
                return false;
            }
            newRPD->key() = newRPD->guid();
            newRPD->RTPSParticipantKey() = participant_guid;
            if (!newRPD->has_locators()) {
                newRPD->set_remote_locators(participant_data.default_locators, localparticipant, true);
            }

            newRPD->setIdlCrc16(crc16Idl);
            return true;
        };

        GUID_t temp_participant_guid;
        ReaderProxyData* reader_data =
            this->mp_PDP->addReaderProxyData(reader_guid, temp_participant_guid, std::move(init_fun));
        if (reader_data != nullptr) {
            (void)this->pairing_reader_proxy_with_any_local_writer(participant_guid, reader_data);
            return true;
        }
    }
    return false;
}

bool EDPStatic::newRemoteWriter(const GUID_t& participant_guid, const string_255& participant_name, uint16_t user_id,
                                EntityId_t ent_id, const GUID_t& persistence_guid, std::string m_crc16Idl) {
    WriterProxyData* wpd = NULL;
    if (mp_edpXML->lookforWriter(participant_name.c_str(), user_id, &wpd) == xmlparser::XMLP_ret::XML_OK) {
        logDebug(RTPS_EDP, "Activating: " << wpd->guid().entityId << " in topic " << wpd->topicName());
        GUID_t writer_guid(participant_guid.guidPrefix, ent_id != c_EntityId_Unknown ? ent_id : wpd->guid().entityId);

        auto init_fun = [this, participant_guid, writer_guid, wpd, persistence_guid, crc16Idl = std::move(m_crc16Idl)](
                            WriterProxyData* newWPD, bool updating, const ParticipantProxyData& participant_data,
                            RTPSParticipantImpl* localparticipant) {
            // Should be a new reader
            (void)updating;
            assert(!updating);

            *newWPD = *wpd;
            newWPD->guid(writer_guid);
            if (!checkEntityId(newWPD)) {
                elogError(RTPS_EDP, RetCode_t::RETCODE_PRECONDITION_NOT_MET,
                          "The provided entityId for Writer with User ID: " << newWPD->userDefinedId()
                                                                            << " does not match the topic Kind");
                return false;
            }
            newWPD->key() = newWPD->guid();
            newWPD->RTPSParticipantKey() = participant_guid;
            if (!newWPD->has_locators()) {
                newWPD->set_remote_locators(participant_data.default_locators, localparticipant, true);
            }
            newWPD->persistence_guid(persistence_guid);

            newWPD->setIdlCrc16(crc16Idl);
            return true;
        };
        GUID_t temp_participant_guid;
        WriterProxyData* writer_data =
            this->mp_PDP->addWriterProxyData(writer_guid, temp_participant_guid, std::move(init_fun));
        if (writer_data != nullptr) {
            (void)this->pairing_writer_proxy_with_any_local_reader(participant_guid, writer_data);
            return true;
        }
    }
    return false;
}

bool EDPStatic::checkEntityId(ReaderProxyData* rdata) {
    if ((rdata->topicKind() == WITH_KEY) && (rdata->guid().entityId.value[3] == 0x07)) {
        return true;
    }
    if ((rdata->topicKind() == NO_KEY) && (rdata->guid().entityId.value[3] == 0x04U)) {
        return true;
    }
    return false;
}

bool EDPStatic::checkEntityId(WriterProxyData* wdata) {
    if ((wdata->topicKind() == WITH_KEY) && (wdata->guid().entityId.value[3] == 0x02U)) {
        return true;
    }
    if ((wdata->topicKind() == NO_KEY) && (wdata->guid().entityId.value[3] == 0x03U)) {
        return true;
    }
    return false;
}

} /* namespace rtps */
} /* namespace ertps */
} /* namespace evbs */
